/*
 * $Id: ShemRecordReader.java,v 1.2 2009/06/22 16:01:41 afaichun Exp $
 * 
 * Copyright (C) 2008 General Electric Company. All Rights Reserved.
 * 
 * This software is the confidential and proprietary information of the General
 * Electric Company (GE). You shall not disclose this software and shall use it
 * only in accordance with the terms of the license agreement you entered into
 * with GE.
 * 
 * GE MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY OF THE
 * SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR
 * NON-INFRINGEMENT. GE SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE
 * AS A RESULT OF USING, MODIFYING, OR DISTRIBUTING THIS SOFTWARE OR ITS
 * DERIVATIVES.
 */
package com.ge.healthcare.autosc.processor.shemjob.reader;

import java.io.DataInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.text.DecimalFormat;

import com.ge.healthcare.autosc.common.ASCUtil;
import com.ge.healthcare.autosc.common.exceptions.PartialRecordIOException;
import com.ge.healthcare.autosc.common.util.ByteSwappingInputFilterStream;
import com.ge.healthcare.autosc.processor.PrcCodeConstants;
import com.ge.healthcare.autosc.processor.exception.ShemReaderException;
import com.ge.healthcare.autosc.processor.magmon.IMagnetMonitorProcessor;
import com.ge.healthcare.autosc.processor.magmon.MagnetMonitorUtility;
import com.ge.healthcare.autosc.processor.shemjob.ShemChannelMap;
import com.ge.healthcare.autosc.processor.shemjob.ShemLogger;
/**
 * SHeM Record Reader.
 *  Reads records from SHeM data files, translating them to various
 *   ASCII representations upon request.
 *
 * @author Daniel S. Horwitz, Compuware Corp.
 *
 * @version AutoSC P5P6
 */
public abstract class ShemRecordReader implements ShemTextReader {
    /**
     * Number of elements of each array type in a SHeM binary record.
     *  This is also the number of channels whose data is recorded in a
     *  SHeM binary record.
     */
    public static final int MAX_FIELDS = 17;

    /**
     * Number of floating-point value fields in an ASCII output record.
     *  It is unclear why there are more output fields than channels in
     *  a binary input record.
     */
    private static final int NUM_OUTPUT_VALUE_FIELDS = 19;

    /**
     * Number of status fields in an ASCII output record.
     */
    private static final int NUM_OUTPUT_STATUS_FIELDS = 16;

    /**
     * Data value to use for an unused channel.
     */
    private static final float UNUSED_CHANNEL_DATA_VALUE = 0.0f;

    /**
     * Status value to use for an unused channel.
     */
    private static final int UNUSED_CHANNEL_STATUS_VALUE = 0x01;

    /**
     * Size of the buffer used to construct an ASCII output record.  This
     *  is large enough so that the buffer does not need to be expanded once
     *  it has been allocated.
     */
    private static final int OUTPUT_BUFFER_SIZE = 512;

    /**
     * Identifier used to refer to the source data stream.
     */
    private String theSourceId = null;

    /**
     * Status field from the SHeM binary minute record format.
     */
    protected short minStatus = 0;

    /**
     * Time field from a SHeM binary record.  This is in UNIX format, which
     *  is the number of seconds since Midnight 1-Jan-1970 UTC.
     */
    protected int rawTime = 0;

    /**
     * Average value fields from a SHeM binary record.  The index of the
     *  element is the channel number.
     */
    protected float[] rawAvgVal = new float[MAX_FIELDS];

    /**
     * Minimum value fields from a SHeM binary record.  These appear only in
     *  the binary hour record format.  When reading other formats, these
     *  are left as zero.  The index of the element is the channel number.
     */
    protected float[] rawMinVal = new float[MAX_FIELDS];

    /**
     * Maximum value fields from a SHeM binary record.  These appear only in
     *  the binary hour record format.  When reading other formats, these
     *  are left as zero.  The index of the element is the channel number.
     */
    protected float[] rawMaxVal = new float[MAX_FIELDS];

    /**
     * Status fields from a SHeM binary record.  These contain the channel
     *  status bits for the corresponding channels.  The index of the
     *  element is the channel number.
     */
    protected byte[] rawStatus = new byte[MAX_FIELDS];

    /**
     * Din field from a SHeM binary record.  When writing this comment, the
     *  author did not know the meaning of Din.
     */
    protected short rawDin = 0;

    /**
     * Dout field from a SHeM binary record.  When writing this comment, the
     *  author did not know the meaning of Dout.
     */
    protected byte rawDout = 0;

    /**
     * Format used for mimicing the C %8.6e format.  Because the format is
     *  based on the locale, the constructor is expensive.  Therefore, a
     *  single format object is used for each record reader instance.
     */
    private DecimalFormat the86eFormat =
        new DecimalFormat( "0.000000E00;-0.000000E00" );

    /**
     * Object that provides utility methods relating to the magnet monitor.
     */
    private MagnetMonitorUtility theUtility = null;

    /**
     * Magnet monitor process object
     */
    private IMagnetMonitorProcessor magMonProcessor = null;
    
    /**
     * Stream for reading the binary data.  The values read from this
     *  stream have already been byte-swapped as appropriate.  The stream
     *  is created in the constructor and closed when the close() method
     *  is messaged.
     */
    private DataInputStream sourceData = null;

    /**
     * Object that provides the mappings from the channel number in the
     *  source binary record to the channel number in the output ASCII
     *  record.
     */
    private ShemChannelMap theChannelMap = new ShemChannelMap();

    /**
     * Construct a SHeM record reader.  It must be openned to use it.
     */
    public ShemRecordReader()
    {
        super();
    }

    /**
     * Open a SHeM record reader.  When done using the reader, the close()
     *  or silentlyClose() methods should be called.
     *
     * @param sourceId Name used to identifier the reader.
     *
     * @param sourceStream The raw input stream for reading the data.  It is the
     *  responsibility of the caller to make sure that this is buffered.
     *
     * @param helper Object providing the utility objects and methods.
     *
     * @param utility Utility class with magnet monitor methods.
     *
     * @param magmon The magnet monitor, which is used to get shem type
     *  and parameter configuration information for configurable readers.
     */
    public void open( String sourceId,
                      InputStream sourceStream,
                      MagnetMonitorUtility utility,
                      IMagnetMonitorProcessor magMonProcessor )
    {
        theUtility = utility;
        theSourceId = sourceId;
        this.magMonProcessor = magMonProcessor;
        int[] elementMap = getElementMap();
        InputStream bsis =
            new ByteSwappingInputFilterStream( sourceStream, elementMap );
        sourceData = new DataInputStream( bsis );
    }

    /**
     * Get element map.  This returns the map of the elements in a source
     *  binary record.  The map specifies the size of the each of the
     *  elements in a source binary record, their location, and the number
     *  of times each element occurs.
     * The map is a series of { number, size } pairs.  The pairs specify the
     *  layout of the record, starting at the beginning of the record.  A
     *  scalar has a number of 1, while an array has a number which is the
     *  number of elements in the array.  Size should be 1 for a byte, 2
     *  for a 16-bit word, 4 for a 32-bit word, or 8 for a 64-bit word.
     * For example, the following C-oriented memory layout is mapped to the
     *  following Java elementMap:
     * <pre>
     *   #define MAX_FIELDS 17             static final int MAX_FIELDS = 17;
     *   packed struct newRec                  new int[] {
     *   {
     *       short minStatus;                     1, 2,
     *       long rawTime;                        1, 4,
     *       float rawAvgVal[MAX_FIELDS];         MAX_FIELDS, 4,
     *       float rawMinVal[MAX_FIELDS];         MAX_FIELDS, 4,
     *       float rawMaxVal[MAX_FIELDS];         MAX_FIELDS, 4,
     *       char rawStatus[MAX_FIELDS];          MAX_FIELDS, 1,
     *       short rawDin;                        1, 2,
     *       char rawDout;                        1, 1<
     *   };                                   }
     * </pre>
     *
     * @return The map of the elements in the source binary record.
     */
    protected abstract int[] getElementMap();

    /**
     * Close the reader.
     *
     * @exception AscServiceAbortException If an error is detected closing
     *  the reader, an error is logged and then an abort exception is thrown.
     */
    public void close() throws ShemReaderException
    {
        if ( sourceData == null )
        {
            return;
        }
        try
        {
            sourceData.close();
        }
        catch ( IOException e )
        {
            String message = "Error closing " + theSourceId + ": " + e;
            throw new ShemReaderException(message);
        }
        finally
        {
            sourceData = null;
        }
    }

    /**
     * Silently close the reader.  This never throws an exception.  If
     *  an I/O error is detected closing a stream, it is not logged.
     *  Other errors may be logged at the discretion of the implementor.
     */
    public void silentlyClose()
    {
        ASCUtil.silentlyClose( sourceData );
        sourceData = null;
    }

    /**
     * Read a line of text.  If the text is read from an underlying reader,
     *  a line is considered to be terminated by any one of a line feed,
     *  a carriage return, or a carriage return followed immediately by
     *  a linefeed.  Text lines can also be constructed by the reader, in
     *  which case the reader defines a text line.  Typically, a text line
     *  is a record from an underlying data source.
     * In this case, read a single binary record and create an output
     *  record from it.  The output record is in the appropriate ASCII
     *  magnet data format.
     *
     * @return
     *  A string containing the contents of the line, not including any
     *   line-termination characters, or null if the end of the data has
     *   been reached.
     *
     * @exception AscServiceAbortException If an error is detected reading
     *  a line of text, an error is logged and then an
     *  abort exception is thrown.
     */
    public String readLine() throws ShemReaderException
    {
    	if(sourceData == null) {
    		throw new ShemReaderException("Record reader not open");
    	}
    	
        try
        {
            readOneRecordBody( sourceData );
        }
        catch ( EOFException eofOccurred )
        {
            return null;
        }
        catch ( IOException ioProblem )
        {
            if ( ioProblem instanceof PartialRecordIOException )
            {
                String warningMessage =
                    theSourceId + " not a whole number of binary records";
                issueWarning(warningMessage);
            }
            String message =
                "Error reading from " + theSourceId + ": " + ioProblem;
            throw new ShemReaderException(message, PrcCodeConstants.PRC_F_SHM_007);
        }
        return toString();
    }

    /**
     * Read the body of a single record into the individual fields.  This
     *  reads all the fields of the source binary record.  The layout of
     *  the source record matches that of the element map supplied to the
     *  constructor.
     *
     * @param sourceStream Stream for reading the binary data.  The values
     *  read from this stream have already been byte-swapped as appropriate.
     *
     * @exception EOFException If the end of the source stream is reached
     *  before the entire body of the record has been read.
     *  This means that partial ending records are ignored.
     *
     * @exception IOException If an I/O error, other than end of stream, occurs.
     */
    protected abstract void readOneRecordBody( DataInputStream sourceStream )
        throws EOFException, IOException;

    /**
     * Called by the garbage collector on an object when garbage collection
     *  determines that there are no more references to the object.  In this
     *  case, this makes sure that this object has been properly closed.
     *
     * @exception Throwable Throws an IOException if an I/O error occurs.
     */
    protected void finalize() throws Throwable
    {
        silentlyClose();
        super.finalize();
    }

    /**
     * Returns a string representation of the object.  In this case, it
     *  returns the most recently read binary source record, translated to
     *  the appropriate ASCII magnet data format.
     * The ASCII magnet data format for a single record is as follows:
     *  * Timestamp of the form: yyyy/mm/dd!hh:mm
     *  * One or two spaces, depending on source record type
     *  * 19 value fields in " %8.6e" format.  The values are the SHeM
     *     values mapped to EDM channels.  Unused channels have a value
     *     of zero.
     *  * 16 status fields.  These are the status values for the first
     *     16 channels.  These values are in the range 0 to 7.
     *
     * @return
     *  A string representation of the object, which is the most recently
     *   read binary source record, translated to the appropriate ASCII
     *   magnet data format.
     */
    public String toString()
    {
        int idx = 0;
        int shemIndex = 0;
        StringBuffer buffer = new StringBuffer( OUTPUT_BUFFER_SIZE );
        buffer.append( brokenOutTime() );
        buffer.append( spacesAfterBrokenOutTime() );
        for ( idx = 0; idx < NUM_OUTPUT_VALUE_FIELDS; idx += 1 )
        {
            shemIndex = theChannelMap.binaryIndexForAsciiIndex( idx );
            float value = UNUSED_CHANNEL_DATA_VALUE;
            if ( shemIndex != ShemChannelMap.UNUSED )
            {
                value = rawAvgVal[shemIndex];
            }
            buffer.append( ' ' );
            buffer.append( to86e( value ) );
        }
        for ( idx = 0; idx < NUM_OUTPUT_STATUS_FIELDS; idx += 1 )
        {
            shemIndex = theChannelMap.binaryIndexForAsciiIndex( idx );
            int statusValue = UNUSED_CHANNEL_STATUS_VALUE;
            if ( shemIndex != ShemChannelMap.UNUSED )
            {
                statusValue = 0x0ff & rawStatus[shemIndex];
            }
            buffer.append( ' ' );
            statusValue = mapBinaryStatusToAscii( statusValue );
            buffer.append( statusValue );
        }
        return buffer.toString();
    }

    protected void issueWarning(String message) {
    	if(magMonProcessor != null) {
    		ShemLogger.warn(message, 
    						"shemteam", 
    						magMonProcessor.getSite(), 
    						magMonProcessor.getEventMinorFunction());
    	}
    }
    
    /**
     * Map the status value from the binary bit mapping to the ASCII
     *  bit mapping.  The ASCII bit mapping must fit in a single decimal
     *  digit.  The following mapping is used:<br>
     *      Binary Bit Mask     Description    ASCII Bit Mask<br>
     *           0x01            Disabled           0x01<br>
     *           0x02            ADC Error          0x02<br>
     *           0x10           RF Present          0x04<br>
     *
     * @param binaryValue Binary value to be mapped.
     *
     * @return Resulting ASCII value after mapping.
     */
    private int mapBinaryStatusToAscii( int binaryValue )
    {
        final int rfPresent = (binaryValue & 0x10) >> 2;
        final int lowBits = binaryValue & 0x03;
        return rfPresent | lowBits;
    }

    /**
     * Returns the spaces that appear immediately after the ASCII timestamp
     *  in the ASCII magnet data format.  This string depends on the source
     *  binary record type.
     *
     * @return
     *   The field following the ASCII timestamp in the ASCII magnet data
     *    format, which consists of zero or more spaces.
     */
    protected abstract String spacesAfterBrokenOutTime();

    /**
     * Converts the timestamp from the most recently read binary source record
     *  to a string representing that time as local time in the format
     *  "yyyy/mm/dd!hh:mm".
     *
     * @return The timestamp, rawTime, as local time in a broken-out format.
     */
    private String brokenOutTime()
    {
        StringBuffer buffer = new StringBuffer( 32 );
        theUtility.appendBrokenOutTimeTo( buffer, rawTime, '/', '!', ':' );
        return buffer.toString();
    }

    /**
     * Format a floating-point value in the format "[-]d.dddddde(+|-)dd".
     *  This mimics the C %8.6e format.  There does not appear to be any
     *  direct way to get a plus sign between the E and the exponent, so
     *  it is forced into the string for a non-negative exponent.  The E
     *  becomes e when the lower-case filter is applied to the result.
     *
     * @param value The floating-point value to be formatted.
     *
     * @return The specified value formatted in %8.6e format.
     */
    private String to86e( float value )
    {
        String myString = the86eFormat.format( value );
        final int eIndex = myString.indexOf( 'E' );
        if ( (eIndex >= 0) && (eIndex < (myString.length() - 1)) )
        {
            if ( myString.charAt( eIndex + 1 ) != '-' )
            {
                myString = myString.substring( 0, eIndex + 1 ) +
                           "+" +
                           myString.substring( eIndex + 1 );
            }
        }
        return myString.toLowerCase();
    }
}
